/**
 * Projekt do předmetu GJA - LATEX TABLE CREATOR
 *
 * Soubor: MultiSpanCellTable.java
 *
 * @author Ondřej Kubal  <xkluba00@stud.fit.vutbr.cz>
 * @author Petr Šimon    <xsimon10@stud.fit.vutbr.cz>
 */

package table;

import javax.swing.table.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.awt.datatransfer.*;
import java.util.*;

/**
 * Implementace rozšířené verze tabulky <code>JTable</code>.
 *   - podpora slučování buněk
 *   - individuální vzhled buňek (písma, rámečky, barvy pro každou buňku zvlášť)
 *   - prodpora pro "RowHeader"
 *   - odstranění některých bugů JTable
 *   - kopírování dat z/do schránky (Excell)
 */
public class MultiSpanCellTable extends JTable
{

     boolean changeEditingCell;
     Vector<Integer> columnSizes;
     
     ClipboardAdapter cba = null; //adaptér pro páráci se schránkou
     
     /**
     * Třída umožňující přesypávání dat z schránky
     */
     public class ClipboardAdapter implements ActionListener
     {
          private static final String LINE_BREAK = "\n";
          private static final String CELL_BREAK = "\t";
          private JTable table = null;
          
          /**
           * Konstruktor
           * @param table rodičovská tabulka pro práci s daty.
           */
          public ClipboardAdapter(JTable table)
          {
               this.table = table;
          }
          
          /**
           * Pokud je aktálně editovaná nějaká buňka, zruší probíhající editaci.
           */
          private void cancelEditing()
          {
               if (table.getCellEditor() != null)
               {
                    table.getCellEditor().cancelCellEditing();
               }
          }
          
          /**
           * Odsraní před kopírováním do schránky formátovací znaky, které by vedli k nekonzistenci textu.
           * @param cell aktuální buňka
           */
          private String escape(Object cell)
          {
               return cell.toString().replace(LINE_BREAK, " ").replace(CELL_BREAK, " ");
          }
          
          /**
           * Kopíruje aktuálně vybraná data do schránky.
           * @param isCut po kopírování vymaže data (Vyjmout).
           */
          public void copyToClipboard(boolean isCut)
          {
               cancelEditing();
               int[] rows = table.getSelectedRows();
               int[] cols = table.getSelectedColumns();
               
               if (cols.length < 1) return;
               
               if (rows.length < 1) return;
               
               StringBuilder buffer = new StringBuilder();
               
               for (int i = 0; i < rows.length; i++)
               {
                    for (int j = 0; j < cols.length; j++)
                    {
                         Object str = table.getValueAt(rows[i], cols[j]);
                         
                         if (str == null) str = "";
                         
                         buffer.append(escape(str));
                         
                         if (isCut)       table.setValueAt(null, rows[i], cols[j]);
                         
                         if (j < cols.length - 1) buffer.append(CELL_BREAK);
                    }
                    
                    buffer.append(LINE_BREAK);
               }
               
               StringSelection sel  = new StringSelection(buffer.toString());
               Toolkit.getDefaultToolkit().getSystemClipboard().setContents(sel, sel);
          }
          
          /**
           * Vloží data ze schránky na pozici začínající levým horním rohem výběru.
           * Přesahy jsou zahozeny.
           */
          public void pasteFromClipboard()
          {
               cancelEditing();
               int colMin = table.getColumnModel().getSelectionModel().getMinSelectionIndex();
               int rowMin = table.getSelectionModel().getMinSelectionIndex();
               int colCnt = table.getColumnCount();
               int rowCnt = table.getRowCount();
               
               String pasteString = "";
               
               try
               {      //pouze neformátovaná tabulková data oddělené tabulátory
                    pasteString = (String)(Toolkit.getDefaultToolkit().getSystemClipboard().getContents(this).getTransferData(DataFlavor.stringFlavor));
               }
               catch (Exception e) //nastala chyba při konverzi dat
               {
                    JOptionPane.showMessageDialog(null, "Invalid Paste Type", "Invalid Paste Type", JOptionPane.ERROR_MESSAGE);
                    return;
               }
               
               String[] lines = pasteString.split(LINE_BREAK);
               table.setRowSelectionInterval(rowMin, Math.min(rowMin + lines.length, rowCnt) - 1);
               
               for (int i = 0 ; i < lines.length; i++)
               {
                    String[] cells = lines[i].split(CELL_BREAK);
                    table.setColumnSelectionInterval(colMin, Math.min(colMin + cells.length, colCnt) - 1);
                    
                    for (int j = 0 ; j < cells.length; j++)
                         if (rowCnt > rowMin + i && colCnt > colMin + j) table.setValueAt(cells[j], rowMin + i, colMin + j);
               }
          }
          
          /**
           * Vymaže hodnoty ve vybrané oblasti.
           */
          public void clear()
          {
               cancelEditing();
               int[] rows = table.getSelectedRows();
               int[] cols = table.getSelectedColumns();
               
               if (cols.length < 1) return;
               
               if (rows.length < 1) return;
               
               for (int i = 0; i < rows.length; i++)
                    for (int j = 0; j < cols.length; j++)
                         table.setValueAt(null, rows[i], cols[j]);
          }
          
          /**
           * Zpracuje vyvolanou akci.
           * @param e akce
           */
          public void actionPerformed(ActionEvent e)
          {
          
               if (e.getActionCommand().compareTo("Copy") == 0)
               {
                    copyToClipboard(false);
               }
               else if (e.getActionCommand().compareTo("Cut") == 0)
               {
                    copyToClipboard(true);
               }
               else if (e.getActionCommand().compareTo("Paste") == 0)
               {
                    pasteFromClipboard();
               }
               else if (e.getActionCommand().compareTo("Clear") == 0)
               {
                    clear();
               }
          }
     }
     
     /**
      * Přístup k vnitřnímu ClipboardAdapter objektu pro ruční vyvolávání událostí kopírování.
      */
     public ClipboardAdapter getClipboardAdapter()
     {
          return cba;
     }
     
     /**
      * Konstruktor.
      * Inicializace UI, kláveskových zkratek, ClipboardAdapteru.
      * @param model Model tabulky, nejlépe AttributiveCellTableModel pro pokročilé vastnosti (ukládání attributů k jednotlivým buňkám)
      */
     public MultiSpanCellTable(TableModel model)
     {
          super(model);
          setUI(new MultiSpanCellTableUI());
          getTableHeader().setReorderingAllowed(false);
          setCellSelectionEnabled(true);
          setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
          
          cba = new ClipboardAdapter(this);
          
          KeyStroke copy = KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK, false);
          KeyStroke cut = KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK, false);
          KeyStroke paste = KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.CTRL_MASK, false);
          KeyStroke clear = KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0, false);
          
          this.registerKeyboardAction(cba, "Copy", copy, JComponent.WHEN_FOCUSED);
          this.registerKeyboardAction(cba, "Cut", cut, JComponent.WHEN_FOCUSED);
          this.registerKeyboardAction(cba, "Paste", paste, JComponent.WHEN_FOCUSED);
          this.registerKeyboardAction(cba, "Clear", clear, JComponent.WHEN_FOCUSED);
          
          changeEditingCell = false;
          //protože nelze udělat úplný overload metody processKeyEvent uděláme si značku, že cellEditing je z ní volán, a že má změnit souřadnice.
     }
     
     
     /**
       * Vrátí Rectangle (velikost a polohu) dané buňky.
       * Upraveno pro potřeby sločování buněk.
       * Počítá i s variabilní velikostí řádku. (opravený bug)
       * @param row index řádku
       * @param column index sloupce
       * @param includeSpacing
       */
     @Override public Rectangle getCellRect(int row, int column, boolean includeSpacing)
     {
          //použijeme nadřazenou funkci pro získání plochy
          Rectangle sRect = super.getCellRect(row, column, includeSpacing);
          
          //ověření, zda se nenacházíme mimo rozsah
          if ((row < 0) || (column < 0) ||
                    (getRowCount() <= row) || (getColumnCount() <= column))
          {
               return sRect;
          }
          
          //vytáhneme hodnoty z modelu
          CellSpan cellAtt = (CellSpan)((AttributiveCellTableModel)getModel()).getCellAttribute();
          
          if (! cellAtt.isVisible(row, column))
          {
               int temp_row    = row;
               int temp_column = column;
               row    += cellAtt.getSpan(temp_row, temp_column)[CellSpan.ROW];
               column += cellAtt.getSpan(temp_row, temp_column)[CellSpan.COLUMN];
          }
          
          //sloučené buňky
          int[] n = cellAtt.getSpan(row, column);
          
          int index = 0;
          int columnMargin = getColumnModel().getColumnMargin();
          
          Rectangle cellFrame = new Rectangle();
          
          //spořítáme vertikální pozici
          cellFrame.y = 0;
          
          for (int i = 0; i < row; i++) cellFrame.y += getRowHeight(i) + rowMargin;
          
          //spočítáme výšku
          cellFrame.height = 0;
          
          for (int i = row; i < (row + n[CellSpan.ROW]); i++) cellFrame.height += getRowHeight(i) + rowMargin;
          
          
          Enumeration enumeration = getColumnModel().getColumns();
          
          //spočítáme horizontální pozici
          while (enumeration.hasMoreElements())
          {
               TableColumn aColumn = (TableColumn)enumeration.nextElement();
               cellFrame.width = aColumn.getWidth() + columnMargin;
               
               if (index == column) break;
               
               cellFrame.x += cellFrame.width;
               index++;
          }
          
          //spočítáme šířku
          for (int i = 0; i < n[CellSpan.COLUMN] - 1; i++)
          {
               TableColumn aColumn = (TableColumn)enumeration.nextElement();
               cellFrame.width += aColumn.getWidth() + columnMargin;
          }
          
          if (!includeSpacing)
          {
               Dimension spacing = getIntercellSpacing();
               cellFrame.setBounds(cellFrame.x +      spacing.width / 2,
                                   cellFrame.y +      spacing.height / 2,
                                   cellFrame.width -  spacing.width,
                                   cellFrame.height - spacing.height);
          }
          
          return cellFrame;
     }
     
     /**
       * Dle pozice myši (point) spočítá aktuální indexy buňky.
       * @param point poloha
       * @return pole ve formátu CellSpan
       */
     private int[] rowColumnAtPoint(Point point)
     {
          int[] retValue = { -1, -1};
          int row = - 1; //jsme mimo rozsah
          int y = point.y;
          
          while (y >= 0) //řádek
          {
               row++;
               
               if ((row < 0) || (getRowCount() <= row)) return retValue;
               
               y -= getRowHeight(row) + rowMargin;
          }
          
          //int row = point.y / (rowHeight + rowMargin);
          
          int column = getColumnModel().getColumnIndexAtX(point.x); //sloupec
          
          CellSpan cellAtt = (CellSpan)((AttributiveCellTableModel)getModel()).getCellAttribute();
          
          if (cellAtt.isVisible(row, column))
          {
               retValue[CellSpan.COLUMN] = column;
               retValue[CellSpan.ROW   ] = row;
               return retValue;
          }
          
          retValue[CellSpan.COLUMN] = column + cellAtt.getSpan(row, column)[CellSpan.COLUMN];
          retValue[CellSpan.ROW   ] = row + cellAtt.getSpan(row, column)[CellSpan.ROW];
          return retValue;
     }
     
     /**
       * Odstranění bugu JTable - přidání sloupce zruší zapamatování šířek sloupců.
       * Uloží šířky sloupců do pomocné struktury.
       */
     public void rememberColWidths()
     {
          int count = this.getColumnCount();
          
          if (columnSizes == null) columnSizes = new Vector<Integer>();
          
          columnSizes.clear();
          
          for (int i = 0; i < count; i++)
               columnSizes.add(this.getColumnModel().getColumn(i).getWidth());
     }
     
     /**
       * Odstranění bugu JTable - přidání sloupce zruší zapamatování šířek sloupců.
       * Načte šířky sloupců z pomocné struktury.
       */
     public void setColWidths()
     {
          if (columnSizes == null) return; //pojistka
          
          int count = Math.min(this.getColumnCount(), columnSizes.size());
          
          for (int i = 0; i < count; i++)
               this.getColumnModel().getColumn(i).setPreferredWidth(((Integer) columnSizes.elementAt(i)).intValue());
     }
     
     /**
       * Dle pozice myši (point) spočítá aktuální řádek.
       * @param point poloha
       * @return číslo řádku
       */
     @Override public int rowAtPoint(Point point)
     {
          return rowColumnAtPoint(point)[CellSpan.ROW];
     }
     
     /**
      * Dle pozice myši (point) spočítá aktuální sloupec.
      * @param point poloha
      * @return číslo sloupce
      */
     @Override public int columnAtPoint(Point point)
     {
          return rowColumnAtPoint(point)[CellSpan.COLUMN];
     }
     
     /**
      * Změna výběru (sloupce) -> překresli
      * @param e událost - nevyužito
      */
     @Override public void columnSelectionChanged(ListSelectionEvent e)
     {
          repaint(); //rezignace na výpočet oblasti -> překresli celé
     }
     
     /**
      * Změna výběru (řádky) -> překresli
      * @param e událost
      */
     @Override public void valueChanged(ListSelectionEvent e)
     {
     
          int firstIndex = e.getFirstIndex();
          int  lastIndex = e.getLastIndex();
          
          if (firstIndex == -1 && lastIndex == -1)   // Selection cleared.
          {
               repaint();
          }
          
          Rectangle dirtyRegion = getCellRect(firstIndex, 0, false);
          int numCoumns = getColumnCount();
          int index = firstIndex;
          
          for (int i = 0; i < numCoumns; i++)
          {
               dirtyRegion.add(getCellRect(index, i, false));
          }
          
          index = lastIndex;
          
          for (int i = 0; i < numCoumns; i++)
          {
               dirtyRegion.add(getCellRect(index, i, false));
          }
          
          repaint(dirtyRegion.x, dirtyRegion.y, dirtyRegion.width, dirtyRegion.height);
     }
     
     /**
      * Upraveno tak, aby byla aktivní vždy nejlevější nejhornější buňka výběru.
      * Jinak stená funkčnost jako v rodiči <code>JTable</code>
      */
     @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column)
     {
     
          Object value = getValueAt(row, column);
          
          boolean isSelected = false;
          boolean hasFocus = false;
          
          // Only indicate the selection and focused cell if not printing
          if (!isPaintingForPrint())
          {
               isSelected = isCellSelected(row, column);
               
               boolean rowIsLead =
                    (selectionModel.getMinSelectionIndex() == row);
               boolean colIsLead =
                    (columnModel.getSelectionModel().getMinSelectionIndex() == column);
                    
               hasFocus = (rowIsLead && colIsLead) && isFocusOwner();
          }
          
          return renderer.getTableCellRendererComponent(this, value,
                    isSelected, hasFocus,
                    row, column);
     }
     
     /**
      * Upraveno tak, aby byla aktivní vždy nejlevější nejhornější buňka výběru.
      * Zabránění editace neviditelnách buněk -> viditelná část.
      * Jinak stená funkčnost jako v rodiči <code>JTable</code>
      * předefinování vyykreslování - změna priority
      */
     @Override public boolean editCellAt(int aRow, int aColumn, EventObject e)
     {
          int row = aRow;
          int col = aColumn;
          
          if (e != null)
          {
               row = getSelectionModel().getMinSelectionIndex();
               col = getColumnModel().getSelectionModel().getMinSelectionIndex();
          }
          
          //předejdeme případné editaci neviditelné buňky
          for (int b = 0; b < 2; b++)
          {
               CellSpan cellAtt = (CellSpan)((AttributiveCellTableModel)getModel()).getCellAttribute();
               int row_offset = ((CellSpan) cellAtt).getSpan(row, col)[CellSpan.ROW];
               int col_offset = ((CellSpan) cellAtt).getSpan(row, col)[CellSpan.COLUMN];
               
               //podíváme se na sloučené buňky
               if (row_offset <= 0) row += row_offset;
               
               if (col_offset <= 0) col += col_offset;
          }
          
          return super.editCellAt(row, col, e);
     }
     
     
     
}
